package com.pz.minioproxy.service;

import com.pz.gulimall.common.exception.RRException;
import com.pz.minioproxy.constant.BigFileConstant;
import com.pz.minioproxy.constant.MinioCodeEnum;
import com.pz.minioproxy.vo.BigFileFeatureReq;
import com.pz.minioproxy.vo.BlobStartRes;
import com.pz.minioproxy.vo.UploadBigFileRes;
import io.minio.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 大文件存储在minio中为分块存储，需要时将分块传输给客户端，客户端将这些分块拼接起来就可以
 * 大文件分块存储格式：{md5第一位字符}/{md5第二位字符}/{md5}/{md5}--{1,2,3...}.suffix
 * 大文件上传完毕后就存储最后一个分块来标记大文件已经上传完毕
 * 标记文件格式: exists/{md5}
 */
@Service
@Slf4j
public class BigFileUploadService {
    @Resource
    private MinioClient minioClient;
    @Resource
    private MinIoService minIoService;
    @Resource
    private ThreadPoolExecutor executor;

    private final String bucketName = BigFileConstant.bucketName;



    /**
     * 创建大文件分块的暂存文件夹
     * @param md5 大文件MD5
     * @param dir 存储文件夹
     */
    private void createBlobFile(String md5, String dir){
        try {
            ObjectWriteResponse objectWriteResponse = minioClient.putObject(
                    PutObjectArgs.builder().bucket(BigFileConstant.bucketName).object(dir).stream(
                                    new ByteArrayInputStream(new byte[]{}), 0, -1)
                            .build());
            if(objectWriteResponse != null) {
                log.info("{} temp blob file created. tag: {}", md5, objectWriteResponse.etag());
            }
        }catch (Exception e) {
            log.error("create blob temp file failed !!!");
            throw new RRException("创建 blob 暂存文件失败");
        }
    }

    /**
     * 创建大文件存在的标记文件
     * @param md5 大文件MD5值
     * @param is 标记输入流 1B
     * @param contentType 标记文件类型，默认为 application/octet-stream
     */
    private void createBigExistFlag(String md5, InputStream is, String contentType) {
        try {
            int len = 1;
            byte []flagBuffer = new byte[len];
            is.read(flagBuffer);
            InputStream flagIs = new ByteArrayInputStream(flagBuffer);
            minIoService.putObject(bucketName, BigFileConstant.getBigFileFlagObjectName(md5),flagIs,len, contentType);
            flagIs.close();
        } catch (IOException e) {
            e.printStackTrace();
            throw new RRException("创建大文件存在标记异常-"+e.getMessage());
        }

    }

    /**
     * 检验大文件是否完整存在
     * @param md5 大文件MD5
     * @return 完整存在:true, 不完整存在:false
     */
    public boolean isBigFileWholeExist(String md5) {
        String bigFileFlagObject = BigFileConstant.getBigFileFlagObjectName(md5);
        return minIoService.isObjectExist(bucketName, bigFileFlagObject);
    }

    /**
     * 获取大文件上传的起始分块索引
     */
    public BlobStartRes getStartBlobIndex(BigFileFeatureReq feature) {
        //1. 检验大文件是否存在
        boolean isExist = isBigFileWholeExist(feature.getMd5());
        try {
            if(isExist){
                log.warn("{} is already existed !", feature.getMd5());
                return new BlobStartRes(true,-1);
            }

            //2. 大文件不存在的情况下，检验大文件分块是否存在
            String blobPathPrefix = BigFileConstant.getBlobPathPrefix(feature.getMd5());
            boolean flag = minIoService.isDirExist(bucketName, blobPathPrefix);
            if(!flag) {
                //2.1 大文件分块不存在：创建一个文件夹来存储该大文件的分块，返回{isExist： false, index: 0, blobPath: {dir}}
                createBlobFile(feature.getMd5() , blobPathPrefix);
                return new BlobStartRes(false, 0);
            } else {
                //2.2 大文件分块存在：返回{isExist: false, index: index-1, blobPath: {dir}}
                long blobNum = (long) Math.ceil(feature.getSize() *1.0 / feature.getMaxBlobSize());

                long l = 0, r = blobNum-1, m;
                while (l <= r) {
                    m = (l+r)/2;
                    String blobObjectName = BigFileConstant.getBlobObjectName(feature.getMd5(), m, feature.getFileName());
                    if(!minIoService.isObjectExist(bucketName, blobObjectName)) {
                        blobObjectName = BigFileConstant.getBlobObjectName(feature.getMd5(), m-1, feature.getFileName());
                        if(minIoService.isObjectExist(bucketName, blobObjectName)){
                            return new BlobStartRes(false, m);
                        } else {
                            r = m-1;
                        }
                    } else {
                        l = m+1;
                    }
                }
                //所有分块集齐
                InputStream is = new ByteArrayInputStream(new byte[]{1});
                createBigExistFlag(feature.getMd5(), is, "application/octet-stream");
                is.close();
                return new BlobStartRes(true, blobNum);
            }
        } catch (Exception e) {
            throw new RRException("获取分块上传文件索引失败");
        }
    }

    /**
     *
     * @param blob 分块
     * @param feature 大文件基本参数
     */
    //上传大文件的分块
    public UploadBigFileRes uploadBigFile(MultipartFile blob, BigFileFeatureReq feature) {
        try {
            // 分块写入到{dir}
            String blobObject = BigFileConstant.getBlobObjectName(feature.getMd5(), feature.getIndex(), feature.getFileName());
            log.info("up blob {} to minio server...", blobObject);
            minIoService.putObject(bucketName, blobObject, blob.getInputStream(), blob.getSize(), blob.getContentType());

            // 2.判断是否是最后一个分块文件
            long blobNum = (long) Math.ceil(feature.getSize() *1.0 / feature.getMaxBlobSize());
            String bigFileObject = BigFileConstant.getBlobPathPrefix(feature.getMd5());
            log.info("{} loading {}/{} ...", bigFileObject, feature.getIndex(), blobNum);
            if (blobNum -1 == feature.getIndex()) {
                // 验证文件完整性
                String blobsPathPrefix = BigFileConstant.getBlobPathPrefix(feature.getMd5());
                log.info("begin to verify the integrity of {}...", blobsPathPrefix);
                AtomicBoolean flag = new AtomicBoolean(true);
                List<Long> records = new ArrayList<>(); //记录缺损文件索引
                List<CompletableFuture<Void>> tasks = new ArrayList<>();
                for (long i = 0; i < blobNum; i++) {
                    // 开始异步操作
                    long finalI = i;
                    CompletableFuture<Void> task = CompletableFuture.runAsync(() -> {
                        log.info("{} verify progress {}/{}", blobsPathPrefix, finalI + 1, blobNum);
                        boolean objectExist = minIoService.isObjectExist(bucketName, BigFileConstant.getBlobObjectName(feature.getMd5(), finalI, feature.getFileName()));
                        if (!objectExist) {
                            flag.set(false);
                            records.add(finalI);
                        }
                    }, executor);
                    tasks.add(task);
                }
                CompletableFuture<Void> combine = CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0]));
                combine.get();
                if(flag.get()) {
                    //文件完整
                    // 是最后的分块,反馈上传完成，并将最后一个分块的1B存储到标记文件，表明这个文件存在
                    log.info("{} verify completed!", bigFileObject);
                    createBigExistFlag(feature.getMd5(), feature.getBlob().getInputStream(), feature.getBlob().getContentType());
                    // TODO 加入中间存储，入库操作
                    return new UploadBigFileRes(1, bigFileObject);
                } else {
                    //文件不完整 提醒客户端文件不完整，从 record 处重新传
                    log.warn("{} file defeat, the index:  {}", BigFileConstant.getBlobPathPrefix(feature.getMd5()), records);
                    return new UploadBigFileRes(-1, bigFileObject, records);
                }

            } else {
                // 2.2 不是最后一个分块，返回未完成上传的反馈
                return new UploadBigFileRes(0, bigFileObject);
            }
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new RRException(MinioCodeEnum.PUT_OBJECT_EXCEPTION.getMsg()+e.getMessage(), MinioCodeEnum.PUT_OBJECT_EXCEPTION.getCode());
        }
    }

}
